/*
 * Copyright 2002-2018 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.java.spring.xml;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.SpringProperties;
import org.springframework.core.convert.support.ConfigurableConversionService;
import org.springframework.core.env.*;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import java.security.AccessControlException;
import java.util.*;

/**
 * Abstract base class for {@link Environment} implementations. Supports the notion of
 * reserved default profile names and enables specifying active and default profiles
 * through the {@link #ACTIVE_PROFILES_PROPERTY_NAME} and
 * {@link #DEFAULT_PROFILES_PROPERTY_NAME} properties.
 *
 * <p>Concrete subclasses differ primarily on which {@link PropertySource} objects they
 * add by default. {@code AbstractEnvironment} adds none. Subclasses should contribute
 * property sources through the protected {@link #customizePropertySources(MutablePropertySources)}
 * hook, while clients should customize using {@link ConfigurableEnvironment#getPropertySources()}
 * and working against the {@link MutablePropertySources} API.
 * See {@link ConfigurableEnvironment} javadoc for usage examples.
 *
 * @author Chris Beams
 * @author Juergen Hoeller
 * @see ConfigurableEnvironment
 * @see StandardEnvironment
 * @since 3.1
 */
public abstract class AbstractEnvironment implements ConfigurableEnvironment {

    /**
     * System property that instructs Spring to ignore system environment variables,
     * i.e. to never attempt to retrieve such a variable via {@link System#getenv()}.
     * <p>The default is "false", falling back to system environment variable checks if a
     * Spring environment property (e.g. a placeholder in a configuration String) isn't
     * resolvable otherwise. Consider switching this flag to "true" if you experience
     * log warnings from {@code getenv} calls coming from Spring, e.g. on WebSphere
     * with strict SecurityManager settings and AccessControlExceptions warnings.
     *
     * @see #suppressGetenvAccess()
     */
    public static final String IGNORE_GETENV_PROPERTY_NAME = "spring.getenv.ignore";

    /**
     * Name of property to set to specify active profiles: {@value}. Value may be comma
     * delimited.
     * <p>Note that certain shell environments such as Bash disallow the use of the period
     * character in variable names. Assuming that Spring's {@link SystemEnvironmentPropertySource}
     * is in use, this property may be specified as an environment variable as
     * {@code SPRING_PROFILES_ACTIVE}.
     *
     * @see ConfigurableEnvironment#setActiveProfiles
     */
    public static final String ACTIVE_PROFILES_PROPERTY_NAME = "spring.profiles.active";

    /**
     * Name of property to set to specify profiles active by default: {@value}. Value may
     * be comma delimited.
     * <p>Note that certain shell environments such as Bash disallow the use of the period
     * character in variable names. Assuming that Spring's {@link SystemEnvironmentPropertySource}
     * is in use, this property may be specified as an environment variable as
     * {@code SPRING_PROFILES_DEFAULT}.
     *
     * @see ConfigurableEnvironment#setDefaultProfiles
     */
    public static final String DEFAULT_PROFILES_PROPERTY_NAME = "spring.profiles.default";

    /**
     * Name of reserved default profile name: {@value}. If no default profile names are
     * explicitly and no active profile names are explicitly set, this profile will
     * automatically be activated by default.
     *
     * @see #getReservedDefaultProfiles
     * @see ConfigurableEnvironment#setDefaultProfiles
     * @see ConfigurableEnvironment#setActiveProfiles
     * @see AbstractEnvironment#DEFAULT_PROFILES_PROPERTY_NAME
     * @see AbstractEnvironment#ACTIVE_PROFILES_PROPERTY_NAME
     */
    protected static final String RESERVED_DEFAULT_PROFILE_NAME = "default";


    protected final Log logger = LogFactory.getLog(getClass());

    private final Set<String> activeProfiles = new LinkedHashSet<>();

    private final Set<String> defaultProfiles = new LinkedHashSet<>(getReservedDefaultProfiles());

    private final MutablePropertySources propertySources = new MutablePropertySources();

    private final ConfigurablePropertyResolver propertyResolver =
            new PropertySourcesPropertyResolver(this.propertySources);


    /**
     * Create a new {@code Environment} instance, calling back to
     * {@link #customizePropertySources(MutablePropertySources)} during construction to
     * allow subclasses to contribute or manipulate {@link PropertySource} instances as
     * appropriate.
     *
     * @see #customizePropertySources(MutablePropertySources)
     */
    public AbstractEnvironment() {
        customizePropertySources(this.propertySources);
    }


    /**
     * Customize the set of {@link PropertySource} objects to be searched by this
     * {@code Environment} during calls to {@link #getProperty(String)} and related
     * methods.
     *
     * <p>Subclasses that override this method are encouraged to add property
     * sources using {@link MutablePropertySources#addLast(PropertySource)} such that
     * further subclasses may call {@code super.customizePropertySources()} with
     * predictable results. For controller:
     * <pre class="code">
     * public class Level1Environment extends AbstractEnvironment {
     *     &#064;Override
     *     protected void customizePropertySources(MutablePropertySources propertySources) {
     *         super.customizePropertySources(propertySources); // no-op from base class
     *         propertySources.addLast(new PropertySourceA(...));
     *         propertySources.addLast(new PropertySourceB(...));
     *     }
     * }
     *
     * public class Level2Environment extends Level1Environment {
     *     &#064;Override
     *     protected void customizePropertySources(MutablePropertySources propertySources) {
     *         super.customizePropertySources(propertySources); // add all from superclass
     *         propertySources.addLast(new PropertySourceC(...));
     *         propertySources.addLast(new PropertySourceD(...));
     *     }
     * }
     * </pre>
     * In this arrangement, properties will be resolved against sources A, B, C, D in that
     * order. That is to say that property source "A" has precedence over property source
     * "D". If the {@code Level2Environment} subclass wished to give property sources C
     * and D higher precedence than A and B, it could simply call
     * {@code super.customizePropertySources} after, rather than before adding its own:
     * <pre class="code">
     * public class Level2Environment extends Level1Environment {
     *     &#064;Override
     *     protected void customizePropertySources(MutablePropertySources propertySources) {
     *         propertySources.addLast(new PropertySourceC(...));
     *         propertySources.addLast(new PropertySourceD(...));
     *         super.customizePropertySources(propertySources); // add all from superclass
     *     }
     * }
     * </pre>
     * The search order is now C, D, A, B as desired.
     *
     * <p>Beyond these recommendations, subclasses may use any of the {@code add&#42;},
     * {@code remove}, or {@code replace} methods exposed by {@link MutablePropertySources}
     * in order to create the exact arrangement of property sources desired.
     *
     * <p>The base implementation registers no property sources.
     *
     * <p>Note that clients of any {@link ConfigurableEnvironment} may further customize
     * property sources via the {@link #getPropertySources()} accessor, typically within
     * an {@link org.springframework.context.ApplicationContextInitializer
     * ApplicationContextInitializer}. For controller:
     * <pre class="code">
     * ConfigurableEnvironment env = new StandardEnvironment();
     * env.getPropertySources().addLast(new PropertySourceX(...));
     * </pre>
     *
     * <h2>A warning about instance variable access</h2>
     * Instance variables declared in subclasses and having default initial values should
     * <em>not</em> be accessed from within this method. Due to Java object creation
     * lifecycle constraints, any initial value will not yet be assigned when this
     * callback is invoked by the {@link #AbstractEnvironment()} constructor, which may
     * lead to a {@code NullPointerException} or other problems. If you need to access
     * default values of instance variables, leave this method as a no-op and perform
     * property source manipulation and instance variable access directly within the
     * subclass constructor. Note that <em>assigning</em> values to instance variables is
     * not problematic; it is only attempting to read default values that must be avoided.
     *
     * @see MutablePropertySources
     * @see PropertySourcesPropertyResolver
     * @see org.springframework.context.ApplicationContextInitializer
     */
    protected void customizePropertySources(MutablePropertySources propertySources) {
    }

    /**
     * Return the set of reserved default profile names. This implementation returns
     * {@value #RESERVED_DEFAULT_PROFILE_NAME}. Subclasses may override in order to
     * customize the set of reserved names.
     *
     * @see #RESERVED_DEFAULT_PROFILE_NAME
     * @see #doGetDefaultProfiles()
     */
    protected Set<String> getReservedDefaultProfiles() {
        return Collections.singleton(RESERVED_DEFAULT_PROFILE_NAME);
    }


    //---------------------------------------------------------------------
    // Implementation of ConfigurableEnvironment interface
    //---------------------------------------------------------------------

    @Override
    public String[] getActiveProfiles() {
        return StringUtils.toStringArray(doGetActiveProfiles());
    }

    /**
     * Return the set of active profiles as explicitly set through
     * {@link #setActiveProfiles} or if the current set of active profiles
     * is empty, check for the presence of the {@value #ACTIVE_PROFILES_PROPERTY_NAME}
     * property and assign its value to the set of active profiles.
     * <p>
     * 如果 activeProfiles 为空，则从 Properties 中获取 spring.profiles.active 配置，如果不为空，
     * 则调用 #setActiveProfiles(String... profiles) 方法，设置 profile，最后返回。
     *
     * @see #getActiveProfiles()
     * @see #ACTIVE_PROFILES_PROPERTY_NAME
     */
    protected Set<String> doGetActiveProfiles() {
        synchronized (this.activeProfiles) {
            // 如果 activeProfiles 为空，则进行初始化
            if (this.activeProfiles.isEmpty()) {
                // 获得 ACTIVE_PROFILES_PROPERTY_NAME 对应的 profiles 属性值
                String profiles = getProperty(ACTIVE_PROFILES_PROPERTY_NAME);
                if (StringUtils.hasText(profiles)) {
                    // 设置到 activeProfiles 中
                    setActiveProfiles(StringUtils.commaDelimitedListToStringArray(
                            StringUtils.trimAllWhitespace(profiles)));
                }
            }
            return this.activeProfiles;
        }
    }

    @Override
    public void setActiveProfiles(String... profiles) {
        Assert.notNull(profiles, "Profile array must not be null");
        if (logger.isDebugEnabled()) {
            logger.debug("Activating profiles " + Arrays.asList(profiles));
        }
        synchronized (this.activeProfiles) {
            // 清空 activeProfiles
            this.activeProfiles.clear();
            // 遍历 profiles 数组，添加到 activeProfiles 中
            for (String profile : profiles) {
                // 校验该方法其实就是操作 activeProfiles 集合，在每次设置之前都会将该集合清空重新添加，添加之前调用
                // #validateProfile(String profile) 方法，对添加的 profile 进行校验，
                validateProfile(profile);
                this.activeProfiles.add(profile);
            }
        }
    }

    @Override
    public void addActiveProfile(String profile) {
        if (logger.isDebugEnabled()) {
            logger.debug("Activating profile '" + profile + "'");
        }
        validateProfile(profile);
        doGetActiveProfiles();
        synchronized (this.activeProfiles) {
            this.activeProfiles.add(profile);
        }
    }


    @Override
    public String[] getDefaultProfiles() {
        return StringUtils.toStringArray(doGetDefaultProfiles());
    }

    /**
     * Return the set of default profiles explicitly set via
     * {@link #setDefaultProfiles(String...)} or if the current set of default profiles
     * consists only of {@linkplain #getReservedDefaultProfiles() reserved default
     * profiles}, then check for the presence of the
     * {@value #DEFAULT_PROFILES_PROPERTY_NAME} property and assign its value (if any)
     * to the set of default profiles.
     *
     * @see #AbstractEnvironment()
     * @see #getDefaultProfiles()
     * @see #DEFAULT_PROFILES_PROPERTY_NAME
     * @see #getReservedDefaultProfiles()
     */
    protected Set<String> doGetDefaultProfiles() {
        synchronized (this.defaultProfiles) {
            if (this.defaultProfiles.equals(getReservedDefaultProfiles())) {
                String profiles = getProperty(DEFAULT_PROFILES_PROPERTY_NAME);
                if (StringUtils.hasText(profiles)) {
                    setDefaultProfiles(StringUtils.commaDelimitedListToStringArray(
                            StringUtils.trimAllWhitespace(profiles)));
                }
            }
            return this.defaultProfiles;
        }
    }

    /**
     * Specify the set of profiles to be made active by default if no other profiles
     * are explicitly made active through {@link #setActiveProfiles}.
     * <p>Calling this method removes overrides any reserved default profiles
     * that may have been added during construction of the environment.
     *
     * @see #AbstractEnvironment()
     * @see #getReservedDefaultProfiles()
     */
    @Override
    public void setDefaultProfiles(String... profiles) {
        Assert.notNull(profiles, "Profile array must not be null");
        synchronized (this.defaultProfiles) {
            this.defaultProfiles.clear();
            for (String profile : profiles) {
                validateProfile(profile);
                this.defaultProfiles.add(profile);
            }
        }
    }

    @Override
    @Deprecated
    public boolean acceptsProfiles(String... profiles) {
        Assert.notEmpty(profiles, "Must specify at least one profile");
        for (String profile : profiles) {
            if (StringUtils.hasLength(profile) && profile.charAt(0) == '!') {
                if (!isProfileActive(profile.substring(1))) {
                    return true;
                }
            } else if (isProfileActive(profile)) {
                return true;
            }
        }
        return false;
    }

    @Override
    public boolean acceptsProfiles(Profiles profiles) {
        Assert.notNull(profiles, "Profiles must not be null");
        return profiles.matches(this::isProfileActive);
    }

    /**
     * Return whether the given profile is active, or if active profiles are empty
     * whether the profile should be active by default.
     *
     * @throws IllegalArgumentException per {@link #validateProfile(String)}
     */
    protected boolean isProfileActive(String profile) {
        validateProfile(profile);
        Set<String> currentActiveProfiles = doGetActiveProfiles();
        return (currentActiveProfiles.contains(profile) ||
                (currentActiveProfiles.isEmpty() && doGetDefaultProfiles().contains(profile)));
    }

    /**
     * 这个校验过程比较弱，子类可以提供更加严格的校验规则。
     * Validate the given profile, called internally prior to adding to the set of
     * active or default profiles.
     * <p>Subclasses may override to impose further restrictions on profile syntax.
     *
     * @throws IllegalArgumentException if the profile is null, empty, whitespace-only or
     *                                  begins with the profile NOT operator (!).
     * @see #acceptsProfiles
     * @see #addActiveProfile
     * @see #setDefaultProfiles
     */
    protected void validateProfile(String profile) {
        if (!StringUtils.hasText(profile)) {
            throw new IllegalArgumentException("Invalid profile [" + profile + "]: must contain text");
        }
        if (profile.charAt(0) == '!') {
            throw new IllegalArgumentException("Invalid profile [" + profile + "]: must not begin with ! operator");
        }
    }

    @Override
    public MutablePropertySources getPropertySources() {
        return this.propertySources;
    }

    @Override
    @SuppressWarnings({"unchecked", "rawtypes"})
    public Map<String, Object> getSystemProperties() {
        try {
            return (Map) System.getProperties();
        } catch (AccessControlException ex) {
            return (Map) new ReadOnlySystemAttributesMap() {
                @Override
                @Nullable
                protected String getSystemAttribute(String attributeName) {
                    try {
                        return System.getProperty(attributeName);
                    } catch (AccessControlException ex) {
                        if (logger.isInfoEnabled()) {
                            logger.info("Caught AccessControlException when accessing system property '" +
                                    attributeName + "'; its value will be returned [null]. Reason: " + ex.getMessage());
                        }
                        return null;
                    }
                }
            };
        }
    }

    @Override
    @SuppressWarnings({"unchecked", "rawtypes"})
    public Map<String, Object> getSystemEnvironment() {
        if (suppressGetenvAccess()) {
            return Collections.emptyMap();
        }
        try {
            return (Map) System.getenv();
        } catch (AccessControlException ex) {
            return (Map) new ReadOnlySystemAttributesMap() {
                @Override
                @Nullable
                protected String getSystemAttribute(String attributeName) {
                    try {
                        return System.getenv(attributeName);
                    } catch (AccessControlException ex) {
                        if (logger.isInfoEnabled()) {
                            logger.info("Caught AccessControlException when accessing system environment variable '" +
                                    attributeName + "'; its value will be returned [null]. Reason: " + ex.getMessage());
                        }
                        return null;
                    }
                }
            };
        }
    }

    /**
     * Determine whether to suppress {@link System#getenv()}/{@link System#getenv(String)}
     * access for the purposes of {@link #getSystemEnvironment()}.
     * <p>If this method returns {@code true}, an empty dummy Map will be used instead
     * of the regular system environment Map, never even trying to call {@code getenv}
     * and therefore avoiding security manager warnings (if any).
     * <p>The default implementation checks for the "spring.getenv.ignore" system property,
     * returning {@code true} if its value equals "true" in any case.
     *
     * @see #IGNORE_GETENV_PROPERTY_NAME
     * @see SpringProperties#getFlag
     */
    protected boolean suppressGetenvAccess() {
        return SpringProperties.getFlag(IGNORE_GETENV_PROPERTY_NAME);
    }

    @Override
    public void merge(ConfigurableEnvironment parent) {
        for (PropertySource<?> ps : parent.getPropertySources()) {
            if (!this.propertySources.contains(ps.getName())) {
                this.propertySources.addLast(ps);
            }
        }
        String[] parentActiveProfiles = parent.getActiveProfiles();
        if (!ObjectUtils.isEmpty(parentActiveProfiles)) {
            synchronized (this.activeProfiles) {
                for (String profile : parentActiveProfiles) {
                    this.activeProfiles.add(profile);
                }
            }
        }
        String[] parentDefaultProfiles = parent.getDefaultProfiles();
        if (!ObjectUtils.isEmpty(parentDefaultProfiles)) {
            synchronized (this.defaultProfiles) {
                this.defaultProfiles.remove(RESERVED_DEFAULT_PROFILE_NAME);
                for (String profile : parentDefaultProfiles) {
                    this.defaultProfiles.add(profile);
                }
            }
        }
    }


    //---------------------------------------------------------------------
    // Implementation of ConfigurablePropertyResolver interface
    //---------------------------------------------------------------------

    @Override
    public ConfigurableConversionService getConversionService() {
        return this.propertyResolver.getConversionService();
    }

    @Override
    public void setConversionService(ConfigurableConversionService conversionService) {
        this.propertyResolver.setConversionService(conversionService);
    }

    @Override
    public void setPlaceholderPrefix(String placeholderPrefix) {
        this.propertyResolver.setPlaceholderPrefix(placeholderPrefix);
    }

    @Override
    public void setPlaceholderSuffix(String placeholderSuffix) {
        this.propertyResolver.setPlaceholderSuffix(placeholderSuffix);
    }

    @Override
    public void setValueSeparator(@Nullable String valueSeparator) {
        this.propertyResolver.setValueSeparator(valueSeparator);
    }

    @Override
    public void setIgnoreUnresolvableNestedPlaceholders(boolean ignoreUnresolvableNestedPlaceholders) {
        this.propertyResolver.setIgnoreUnresolvableNestedPlaceholders(ignoreUnresolvableNestedPlaceholders);
    }

    @Override
    public void setRequiredProperties(String... requiredProperties) {
        this.propertyResolver.setRequiredProperties(requiredProperties);
    }

    @Override
    public void validateRequiredProperties() throws MissingRequiredPropertiesException {
        this.propertyResolver.validateRequiredProperties();
    }


    //---------------------------------------------------------------------
    // Implementation of PropertyResolver interface
    //---------------------------------------------------------------------

    @Override
    public boolean containsProperty(String key) {
        return this.propertyResolver.containsProperty(key);
    }

    @Override
    @Nullable
    public String getProperty(String key) {
        return this.propertyResolver.getProperty(key);
    }

    @Override
    public String getProperty(String key, String defaultValue) {
        return this.propertyResolver.getProperty(key, defaultValue);
    }

    @Override
    @Nullable
    public <T> T getProperty(String key, Class<T> targetType) {
        return this.propertyResolver.getProperty(key, targetType);
    }

    @Override
    public <T> T getProperty(String key, Class<T> targetType, T defaultValue) {
        return this.propertyResolver.getProperty(key, targetType, defaultValue);
    }

    @Override
    public String getRequiredProperty(String key) throws IllegalStateException {
        return this.propertyResolver.getRequiredProperty(key);
    }

    @Override
    public <T> T getRequiredProperty(String key, Class<T> targetType) throws IllegalStateException {
        return this.propertyResolver.getRequiredProperty(key, targetType);
    }

    @Override
    public String resolvePlaceholders(String text) {
        return this.propertyResolver.resolvePlaceholders(text);
    }

    @Override
    public String resolveRequiredPlaceholders(String text) throws IllegalArgumentException {
        return this.propertyResolver.resolveRequiredPlaceholders(text);
    }


    @Override
    public String toString() {
        return getClass().getSimpleName() + " {activeProfiles=" + this.activeProfiles +
                ", defaultProfiles=" + this.defaultProfiles + ", propertySources=" + this.propertySources + "}";
    }

}
